package com.bumptech.glide.load.engine;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;

import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.Key;

import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * An exception with zero or more causes indicating why a load in Glide failed.
 * <p>
 * 用零个或多个原因来表示Glide为什么加载失败
 */
// Public API.
@SuppressWarnings("WeakerAccess")
public final class GlideException extends Exception {
    private static final long serialVersionUID = 1L;

    private static final StackTraceElement[] EMPTY_ELEMENTS = new StackTraceElement[0];

    private final List<Throwable> causes;
    private Key key;
    private DataSource dataSource;
    private Class<?> dataClass;
    private String detailMessage;
    @Nullable
    private Exception exception;

    public GlideException(String message) {
        this(message, Collections.<Throwable>emptyList());
    }

    public GlideException(String detailMessage, Throwable cause) {
        this(detailMessage, Collections.singletonList(cause));
    }

    public GlideException(String detailMessage, List<Throwable> causes) {
        this.detailMessage = detailMessage;
        setStackTrace(EMPTY_ELEMENTS);
        this.causes = causes;
    }

    void setLoggingDetails(Key key, DataSource dataSource) {
        setLoggingDetails(key, dataSource, null);
    }

    void setLoggingDetails(Key key, DataSource dataSource, Class<?> dataClass) {
        this.key = key;
        this.dataSource = dataSource;
        this.dataClass = dataClass;
    }

    /**
     * Sets a stack trace that includes where the request originated.
     *
     * <p>This is an experimental API that may be removed in the future.
     */
    public void setOrigin(@Nullable Exception exception) {
        this.exception = exception;
    }

    /**
     * Returns an {@link Exception} with a stack trace that includes where the request originated
     * (if previously set via {@link #setOrigin(Exception)})
     *
     * <p>This is an experimental API that may be removed in the future.
     */
    @Nullable
    public Exception getOrigin() {
        return exception;
    }

    // No need to synchronize when doing nothing whatsoever.
    @SuppressWarnings("UnsynchronizedOverridesSynchronized")
    @Override
    public Throwable fillInStackTrace() {
        // Avoid an expensive allocation by doing nothing here. Causes should contain all relevant
        // stack traces.
        return this;
    }

    /**
     * Returns a list of causes that are immediate children of this exception.
     *
     * <p>Causes may or may not be {@link GlideException GlideExceptions}. Causes may also not be root
     * causes, and in turn my have been caused by other failures.</p>
     *
     * @see #getRootCauses()
     */
    public List<Throwable> getCauses() {
        return causes;
    }

    /**
     * Returns the list of root causes that are the leaf nodes of all children of this exception.
     *
     * <p>Use this method to do things like look for http exceptions that indicate the load may have
     * failed due to an error that can be retried. Keep in mind that because Glide may attempt to load
     * a given model using multiple different pathways, there may be multiple related or unrelated
     * reasons for a load to fail.
     */
    public List<Throwable> getRootCauses() {
        List<Throwable> rootCauses = new ArrayList<>();
        addRootCauses(this, rootCauses);
        return rootCauses;
    }

    /**
     * Logs all root causes using the given tag.
     *
     * <p>Each root cause is logged separately to avoid throttling. {@link #printStackTrace()} will
     * provide a more succinct overview of why the exception occurred, although it does not include
     * complete stack traces.
     */
    public void logRootCauses(String tag) {
        List<Throwable> causes = getRootCauses();
        for (int i = 0, size = causes.size(); i < size; i++) {
            Log.i(tag, "Root cause (" + (i + 1) + " of " + size + ")", causes.get(i));
        }
    }

    private void addRootCauses(Throwable throwable, List<Throwable> rootCauses) {
        if (throwable instanceof GlideException) {
            GlideException glideException = (GlideException) throwable;
            for (Throwable t : glideException.getCauses()) {
                addRootCauses(t, rootCauses);
            }
        } else {
            rootCauses.add(throwable);
        }
    }

    @Override
    public void printStackTrace() {
        printStackTrace(System.err);
    }

    @Override
    public void printStackTrace(PrintStream err) {
        printStackTrace((Appendable) err);
    }

    @Override
    public void printStackTrace(PrintWriter err) {
        printStackTrace((Appendable) err);
    }

    private void printStackTrace(Appendable appendable) {
        appendExceptionMessage(this, appendable);
        appendCauses(getCauses(), new IndentedAppendable(appendable));
    }

    // PMD doesn't seem to notice that we're allocating the builder with the suggested size.
    @SuppressWarnings("PMD.InsufficientStringBufferDeclaration")
    @Override
    public String getMessage() {
        StringBuilder result = new StringBuilder(71)
                .append(detailMessage)
                .append(dataClass != null ? ", " + dataClass : "")
                .append(dataSource != null ? ", " + dataSource : "")
                .append(key != null ? ", " + key : "");

        List<Throwable> rootCauses = getRootCauses();
        if (rootCauses.isEmpty()) {
            return result.toString();
        } else if (rootCauses.size() == 1) {
            result.append("\nThere was 1 cause:");
        } else {
            result.append("\nThere were ").append(rootCauses.size()).append(" causes:");
        }
        for (Throwable cause : rootCauses) {
            result.append('\n')
                    .append(cause.getClass().getName()).append('(').append(cause.getMessage()).append(')');
        }
        result.append("\n call GlideException#logRootCauses(String) for more detail");
        return result.toString();
    }

    // Appendable throws, PrintWriter, PrintStream, and IndentedAppendable do not, so this should
    // never happen.
    @SuppressWarnings("PMD.PreserveStackTrace")
    private static void appendExceptionMessage(Throwable t, Appendable appendable) {
        try {
            appendable.append(t.getClass().toString()).append(": ").append(t.getMessage()).append('\n');
        } catch (IOException e1) {
            throw new RuntimeException(t);
        }
    }

    // Appendable throws, PrintWriter, PrintStream, and IndentedAppendable do not, so this should
    // never happen.
    @SuppressWarnings("PMD.PreserveStackTrace")
    private static void appendCauses(List<Throwable> causes, Appendable appendable) {
        try {
            appendCausesWrapped(causes, appendable);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
    private static void appendCausesWrapped(List<Throwable> causes, Appendable appendable)
            throws IOException {
        int size = causes.size();
        for (int i = 0; i < size; i++) {
            appendable.append("Cause (")
                    .append(String.valueOf(i + 1))
                    .append(" of ")
                    .append(String.valueOf(size))
                    .append("): ");

            Throwable cause = causes.get(i);
            if (cause instanceof GlideException) {
                GlideException glideCause = (GlideException) cause;
                glideCause.printStackTrace(appendable);
            } else {
                appendExceptionMessage(cause, appendable);
            }
        }
    }

    private static final class IndentedAppendable implements Appendable {
        private static final String EMPTY_SEQUENCE = "";
        private static final String INDENT = "  ";
        private final Appendable appendable;
        private boolean printedNewLine = true;

        IndentedAppendable(Appendable appendable) {
            this.appendable = appendable;
        }

        @Override
        public Appendable append(char c) throws IOException {
            if (printedNewLine) {
                printedNewLine = false;
                appendable.append(INDENT);
            }
            printedNewLine = c == '\n';
            appendable.append(c);
            return this;
        }

        @Override
        public Appendable append(@Nullable CharSequence charSequence) throws IOException {
            charSequence = safeSequence(charSequence);
            return append(charSequence, 0, charSequence.length());
        }

        @Override
        public Appendable append(@Nullable CharSequence charSequence, int start, int end)
                throws IOException {
            charSequence = safeSequence(charSequence);
            if (printedNewLine) {
                printedNewLine = false;
                appendable.append(INDENT);
            }
            printedNewLine = charSequence.length() > 0 && charSequence.charAt(end - 1) == '\n';
            appendable.append(charSequence, start, end);
            return this;
        }

        @NonNull
        private CharSequence safeSequence(@Nullable CharSequence sequence) {
            if (sequence == null) {
                return EMPTY_SEQUENCE;
            } else {
                return sequence;
            }
        }
    }
}
